<?php
/**
 * 服务器端访问网络的用户代理
 * 
 * ============================================================================
 * 版权所有: (c) 2009-2011，<UEShop Inc>，并保留所有权利。
 * 网站地址: http://www.ueshop.cn
 * ----------------------------------------------------------------------------
 * 这不是一个自由软件！您只能在不用于商业目的的前提下对程序代码进行修改和使用；
 * 不允许对程序代码以任何形式任何目的的再发布。
 * ============================================================================
 *
 * @author      cmpan
 * @copyright   Copyright (c) 2009-2011 UEShop Inc. (http://www.ueshop.cn)
 * @license     http://www.ueshop.cn/1_0.txt
 */
class UE_IP_Exception extends UE_Exception {}
class UE_IP {
	/**
     * QQWry.Dat文件指针
     *
     * @var resource
     */
	public $fp;

	/**
     * 第一条IP记录的偏移地址
     *
     * @var int
     */
	public $firstIP;

	/**
     * 最后一条IP记录的偏移地址
     *
     * @var int
     */
	public $lastIP;

	/**
     * IP记录的总条数（不包含版本信息记录）
     *
     * @var int
     */
	public $totalIP;
	
	/**
     * 返回读取的长整型数
     *
     * @access private
     * @return int
     */
	private function getLong() {
		//将读取的little-endian编码的4个字节转化为长整型数
		$result = unpack('Vlong', fread($this->fp, 4));
		return $result['long'];
	}

	/**
     * 返回读取的3个字节的长整型数
     *
     * @access private
     * @return int
     */
	private function getLong3() {
		//将读取的little-endian编码的3个字节转化为长整型数
		$result = unpack('Vlong', fread($this->fp, 3).chr(0));
		return $result['long'];
	}

	/**
     * 返回读取的字符串
     *
     * @access private
     * @param string $data
     * @return string
     */
	private function getString($data = "") {
		$char = fread($this->fp, 1);
		while (ord($char) > 0) {        // 字符串按照C格式保存，以\0结束
			$data .= $char;             // 将读取的字符连接到给定字符串之后
			$char = fread($this->fp, 1);
		}
		return $data;
	}

	/**
     * 返回地区信息
     *
     * @access private
     * @return string
     */
	private function _getArea() {
		$byte = fread($this->fp, 1);    // 标志字节
		switch (ord($byte)) {
			case 0:                     // 没有区域信息
			    $area = "";
			    break;
			case 1:
			case 2:                     // 标志字节为1或2，表示区域信息被重定向
			    fseek($this->fp, $this->getLong3());
			    $area = $this->getString();
			    break;
			default:                    // 否则，表示区域信息没有被重定向
			    $area = $this->getString($byte);
			    break;
		}
		
		return $area;
	}

	/**
     * 根据所给 IP 地址或域名返回所在地区信息
     * 
     * 返回结果:
     * Array (
     *     [IP] => 210.36.16.33
     *     [beginIP] => 210.36.16.0
     *     [endIP] => 210.36.31.255
     *     [location] => 广西南宁市
     *     [area] => 广西大学
     * )
     * 
     * @access public
     * @return Array 
     * 
     */
	public function get($IP = '') {
	    $IP = trim($IP);
		$IP = $IP ? $IP : self::realIP();
		//if (!$this->fp) return null;            // 如果数据文件没有被正确打开，则直接返回空
		// 如果数据文件没有被正确打开，返回ip xxx.xxx.*.*
		if (!$this->fp) {
			$location['IP'] = $IP;
			$location['area'] = $location['location'] = preg_replace("/(\d+?\.\d+?\.).+/", "\\1*.*", $IP);;
			return $location;            
		}
		$location['IP'] = gethostbyname($IP);   // 将输入的域名转化为IP地址
		$IP = pack('N', intval(ip2long($location['IP'])));   // 将输入的IP地址转化为可比较的IP地址
		// 不合法的IP地址会被转化为255.255.255.255
		// 对分搜索
		$l = 0;                         // 搜索的下边界
		$u = $this->totalIP;            // 搜索的上边界
		$findIP = $this->lastIP;        // 如果没有找到就返回最后一条IP记录（QQWry.Dat的版本信息）
		while ($l <= $u) {              // 当上边界小于下边界时，查找失败
			$i = floor(($l + $u) / 2);  // 计算近似中间记录
			fseek($this->fp, $this->firstIP + $i * 7);
			$beginIP = strrev(fread($this->fp, 4));     // 获取中间记录的开始IP地址
			// strrev函数在这里的作用是将little-endian的压缩IP地址转化为big-endian的格式
			// 以便用于比较，后面相同。
			if ($IP < $beginIP) {       // 用户的IP小于中间记录的开始IP地址时
				$u = $i - 1;            // 将搜索的上边界修改为中间记录减一
			}
			else {
				fseek($this->fp, $this->getLong3());
				$endIP = strrev(fread($this->fp, 4));   // 获取中间记录的结束IP地址
				if ($IP > $endIP) {     // 用户的IP大于中间记录的结束IP地址时
					$l = $i + 1;        // 将搜索的下边界修改为中间记录加一
				}
				else {                  // 用户的IP在中间记录的IP范围内时
					$findIP = $this->firstIP + $i * 7;
					break;              // 则表示找到结果，退出循环
				}
			}
		}

		//获取查找到的IP地理位置信息
		fseek($this->fp, $findIP);
		$location['beginIP'] = long2IP($this->getLong());   // 用户IP所在范围的开始地址
		$offset = $this->getLong3();
		fseek($this->fp, $offset);
		$location['endIP'] = long2IP($this->getLong());     // 用户IP所在范围的结束地址
		$byte = fread($this->fp, 1);    // 标志字节
		switch (ord($byte)) {
			case 1:                     // 标志字节为1，表示国家和区域信息都被同时重定向
			$countryOffset = $this->getLong3();         // 重定向地址
			fseek($this->fp, $countryOffset);
			$byte = fread($this->fp, 1);    // 标志字节
			switch (ord($byte)) {
				case 2:             // 标志字节为2，表示国家信息又被重定向
				fseek($this->fp, $this->getLong3());
				$location['location'] = $this->getString();
				fseek($this->fp, $countryOffset + 4);
				$location['area'] = $this->_getArea();
				break;
				default:            // 否则，表示国家信息没有被重定向
				$location['location'] = $this->getString($byte);
				$location['area'] = $this->_getArea();
				break;
			}
			break;
			case 2:                     // 标志字节为2，表示国家信息被重定向
			fseek($this->fp, $this->getLong3());
			$location['location'] = $this->getString();
			fseek($this->fp, $offset + 8);
			$location['area'] = $this->_getArea();
			break;
			default:                    // 否则，表示国家信息没有被重定向
			$location['location'] = $this->getString($byte);
			$location['area'] = $this->_getArea();
			break;
		}
		if ($location['location'] == " CZ88.NET") {  // CZ88.NET表示没有有效信息
			$location['location'] = 'Unknown';
		}
		if ($location['area'] == " CZ88.NET") {
			$location['area'] = "";
		}

        $location['area'] = gbk2utf8($location['area']);
		$location['location'] = gbk2utf8($location['location']);
		
		return $location;
	}

	/**
     * 构造函数，打开 QQWry.Dat 文件并初始化类中的信息
     *
     * @param string $filename
     * @return IP
     */
	public function __construct($filename = '') {
		if (empty($filename)) {
			$filename = dirname(__FILE__) . '/_data/QQWry.Dat';
		}
		
		if(!is_file($filename)) {
			$this->fp = null;
			return;
		}
		
		$this->fp = 0;
		if (($this->fp = @fopen($filename, 'rb')) !== false) {
			$this->firstIP = $this->getLong();
			$this->lastIP  = $this->getLong();
			$this->totalIP = ($this->lastIP - $this->firstIP)/7;
		} else {
		    throw new UE_IP_Exception('无法打开数据库文件:' . $filename);
		}
	}

	/**
     * 析构函数，用于在页面执行结束后自动关闭打开的文件。
     *
     */
	public function __destruct() {
		if ($this->fp) {
			fclose($this->fp);
		}
		$this->fp = 0;
	}


	/**
	 * 客户端真实IP
	 *
	 * @return string
	 */
	public static function realIp( ) {
		static $realIp = NULL;
		
		if ($realIp !== NULL) {
			return $realIp;
		}

		if (isset($_SERVER)) {
			if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
				$arr = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
				foreach ($arr as $ip) {
					$ip = trim($ip);
					if ($ip != "unknown") {
						$realIp = $ip;
						break;
					}
				}
			} elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
				$realIp = $_SERVER['HTTP_CLIENT_IP'];
			} elseif (isset($_SERVER['REMOTE_ADDR'])) {
				$realIp = $_SERVER['REMOTE_ADDR'];
			} else {
				$realIp = "0.0.0.0";
			}
		} elseif (getenv("HTTP_X_FORWARDED_FOR")) {
			$realIp = getenv("HTTP_X_FORWARDED_FOR");
		} elseif (getenv("HTTP_CLIENT_IP")) {
			$realIp = getenv("HTTP_CLIENT_IP");
		} else {
			$realIp = getenv("REMOTE_ADDR");
		}

		preg_match( "/[\\d\\.]{7,15}/", $realIp, $onlineIp);
		$realIp = !empty($onlineIp[0]) ? $onlineIp[0] : '0.0.0.0';

		return $realIp;
	}

	/**
	 * 获取IP库版本
	 * 
	 * 返回格式
	 * 2007-8-30
	 *
	 * @return string
	 */
	public function getVersion() {
		$IP = '255.255.255.000';
		$rs = $this->get($IP);
		$rs = $rs['area'];
		$rs = str_replace(array('日IP数据', '年', '月'), array('', '-', '-'), $rs);
		return $rs;
	}

	/**
	 * 传入IP
	 * 获取地理位置
	 *
	 * @param String $IP
	 * @return String 地理位置
	 */
	public function getLocation($IP = '') {
		$IP = $IP ?  $IP : self::realIP();
		$location = $this->get($IP);
		if(!$location) {
			return null;
		}
		return $location['location'];
	}

	/**
	 * 传入IP
	 * 返回IP的地理区域
	 *
	 * @param String $IP
	 * @return string
	 */
	public function getArea($IP = '') {
		$IP = (!empty($IP)) ? self::realIP() : $IP;
		$location = $this->get($IP);
		
		if(!$location) {
			return null;
		}
		
		return $location['area'];
	}
}

